/*
 * %W% %E%
 *
 * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package java.beans;

import java.lang.ref.Reference;

import java.lang.reflect.Method;
import java.lang.reflect.Constructor;

/**
 * A PropertyDescriptor describes one property that a Java Bean
 * exports via a pair of accessor methods.
 */
public class PropertyDescriptor extends FeatureDescriptor {

    private Reference propertyTypeRef;
    private Reference readMethodRef;
    private Reference writeMethodRef;
    private Reference propertyEditorClassRef;

    private boolean bound;
    private boolean constrained;

    // The base name of the method name which will be prefixed with the
    // read and write method. If name == "foo" then the baseName is "Foo"
    private String baseName;

    private String writeMethodName;
    private String readMethodName;

    /**
     * Constructs a PropertyDescriptor for a property that follows
     * the standard Java convention by having getFoo and setFoo
     * accessor methods.  Thus if the argument name is "fred", it will
     * assume that the writer method is "setFred" and the reader method
     * is "getFred" (or "isFred" for a boolean property).  Note that the
     * property name should start with a lower case character, which will
     * be capitalized in the method names.
     *
     * @param propertyName The programmatic name of the property.
     * @param beanClass The Class object for the target bean.  For
     *		example sun.beans.OurButton.class.
     * @exception IntrospectionException if an exception occurs during
     *              introspection.
     */
    public PropertyDescriptor(String propertyName, Class<?> beanClass)
		throws IntrospectionException {
	this(propertyName, beanClass, 
	     "is" + capitalize(propertyName), 
	     "set" + capitalize(propertyName));
    }

    /**
     * This constructor takes the name of a simple property, and method
     * names for reading and writing the property.
     *
     * @param propertyName The programmatic name of the property.
     * @param beanClass The Class object for the target bean.  For
     *		example sun.beans.OurButton.class.
     * @param readMethodName The name of the method used for reading the property
     *		 value.  May be null if the property is write-only.
     * @param writeMethodName The name of the method used for writing the property
     *		 value.  May be null if the property is read-only.
     * @exception IntrospectionException if an exception occurs during
     *              introspection.
     */
    public PropertyDescriptor(String propertyName, Class<?> beanClass,
		String readMethodName, String writeMethodName)
		throws IntrospectionException {
	if (beanClass == null) {
	    throw new IntrospectionException("Target Bean class is null");
	}
	if (propertyName == null || propertyName.length() == 0) {
	    throw new IntrospectionException("bad property name");
	}
	if ("".equals(readMethodName) || "".equals(writeMethodName)) {
	    throw new IntrospectionException("read or write method name should not be the empty string");
	}	    
	setName(propertyName);
	setClass0(beanClass);

	this.readMethodName = readMethodName;
	if (readMethodName != null && getReadMethod() == null) {
	    throw new IntrospectionException("Method not found: " + readMethodName);
	}
	this.writeMethodName = writeMethodName;
	if (writeMethodName != null && getWriteMethod() == null) {
	    throw new IntrospectionException("Method not found: " + writeMethodName);
	}
	
    }

    /**
     * This constructor takes the name of a simple property, and Method
     * objects for reading and writing the property.
     *
     * @param propertyName The programmatic name of the property.
     * @param readMethod The method used for reading the property value.
     *		May be null if the property is write-only.
     * @param writeMethod The method used for writing the property value.
     *		May be null if the property is read-only.
     * @exception IntrospectionException if an exception occurs during
     *              introspection.
     */
    public PropertyDescriptor(String propertyName, Method readMethod, Method writeMethod)
		throws IntrospectionException {
	if (propertyName == null || propertyName.length() == 0) {
	    throw new IntrospectionException("bad property name");
	}
	setName(propertyName);
	setReadMethod(readMethod);
	setWriteMethod(writeMethod);
    }

    /**
     * Gets the Class object for the property.
     *
     * @return The Java type info for the property.  Note that
     * the "Class" object may describe a built-in Java type such as "int".
     * The result may be "null" if this is an indexed property that
     * does not support non-indexed access.
     * <p>
     * This is the type that will be returned by the ReadMethod.
     */
    public synchronized Class<?> getPropertyType() {
	Class type = getPropertyType0();
	if (type  == null) {
	    try {
		type = findPropertyType(getReadMethod(), getWriteMethod());
		setPropertyType(type);
	    } catch (IntrospectionException ex) {
		// Fall 
	    }
	}
	return type;
    }

    private void setPropertyType(Class type) {
	propertyTypeRef = createReference(type);
    }

    private Class getPropertyType0() {
	return (Class)getObject(propertyTypeRef);
    }

    /**
     * Gets the method that should be used to read the property value.
     *
     * @return The method that should be used to read the property value.
     * May return null if the property can't be read.
     */
    public synchronized Method getReadMethod() {
	Method readMethod = getReadMethod0();
	if (readMethod == null) {
	    Class cls = getClass0();
	    if (cls == null || (readMethodName == null && readMethodRef == null)) {
		// The read method was explicitly set to null.
		return null;
	    }
	    if (readMethodName == null) {
		Class type = getPropertyType0();
		if (type == boolean.class || type == null) {
		    readMethodName = "is" + getBaseName();
		} else {
		    readMethodName = "get" + getBaseName();
		}
	    }
	    
	    // Since there can be multiple write methods but only one getter
	    // method, find the getter method first so that you know what the
	    // property type is.  For booleans, there can be "is" and "get"
	    // methods.  If an "is" method exists, this is the official
	    // reader method so look for this one first.
	    readMethod = Introspector.findMethod(cls, readMethodName, 0);
	    if (readMethod == null) {
		readMethodName = "get" + getBaseName();
		readMethod = Introspector.findMethod(cls, readMethodName, 0);
	    }
	    try {
		setReadMethod(readMethod);
	    } catch (IntrospectionException ex) {
		// fall
	    }
	}
	return readMethod;
    }

    /**
     * Sets the method that should be used to read the property value.
     *
     * @param readMethod The new read method.
     */
    public synchronized void setReadMethod(Method readMethod)
				throws IntrospectionException {
	if (readMethod == null) {
	    readMethodName = null;
	    readMethodRef = null;
	    return;
	}
	// The property type is determined by the read method.
	setPropertyType(findPropertyType(readMethod, getWriteMethod0()));
	setClass0(readMethod.getDeclaringClass());

	readMethodName = readMethod.getName();
	readMethodRef = createReference(readMethod, true);
    }

    /**
     * Gets the method that should be used to write the property value.
     *
     * @return The method that should be used to write the property value.
     * May return null if the property can't be written.
     */
    public synchronized Method getWriteMethod() {
	Method writeMethod = getWriteMethod0();
	if (writeMethod == null) {
	    Class cls = getClass0();
	    if (cls == null || (writeMethodName == null && writeMethodRef == null)) {
		// The write method was explicitly set to null.
		return null;
	    }

	    // We need the type to fetch the correct method.
	    Class type = getPropertyType0();
	    if (type == null) {
		try {
		    // Can't use getPropertyType since it will lead to recursive loop.
		    type = findPropertyType(getReadMethod(), null);
		    setPropertyType(type);
		} catch (IntrospectionException ex) {
		    // Without the correct property type we can't be guaranteed 
		    // to find the correct method.
		    return null;
		}
	    }
		    
	    if (writeMethodName == null) {
		writeMethodName = "set" + getBaseName();
	    }

	    writeMethod = Introspector.findMethod(cls, writeMethodName, 1, 
   			  (type == null) ? null : new Class[] { type });
	    try {
		setWriteMethod(writeMethod);
	    } catch (IntrospectionException ex) {
		// fall through
	    }
	}
	return writeMethod;
    }

    /**
     * Sets the method that should be used to write the property value.
     *
     * @param writeMethod The new write method.
     */
    public synchronized void setWriteMethod(Method writeMethod)
				throws IntrospectionException {
	if (writeMethod == null) {
	    writeMethodName = null;
	    writeMethodRef = null;
	    return;
	}
	// Set the property type - which validates the method
	setPropertyType(findPropertyType(getReadMethod(), writeMethod));
	setClass0(writeMethod.getDeclaringClass());

	writeMethodName = writeMethod.getName();
	writeMethodRef = createReference(writeMethod, true);

    }

    private Method getReadMethod0() {
	return (Method)getObject(readMethodRef);
    }

    private Method getWriteMethod0() {
	return (Method)getObject(writeMethodRef);
    }

    /**
     * Overridden to ensure that a super class doesn't take precedent
     */
    void setClass0(Class clz) {
	if (getClass0() != null && clz.isAssignableFrom(getClass0())) {
	    // dont replace a subclass with a superclass
	    return;
	}
	super.setClass0(clz);
    }

    /**
     * Updates to "bound" properties will cause a "PropertyChange" event to
     * get fired when the property is changed.
     *
     * @return True if this is a bound property.
     */
    public boolean isBound() {
	return bound;
    }

    /**
     * Updates to "bound" properties will cause a "PropertyChange" event to
     * get fired when the property is changed.
     *
     * @param bound True if this is a bound property.
     */
    public void setBound(boolean bound) {
	this.bound = bound;
    }

    /**
     * Attempted updates to "Constrained" properties will cause a "VetoableChange"
     * event to get fired when the property is changed.
     *
     * @return True if this is a constrained property.
     */
    public boolean isConstrained() {
	return constrained;
    }

    /**
     * Attempted updates to "Constrained" properties will cause a "VetoableChange"
     * event to get fired when the property is changed.
     *
     * @param constrained True if this is a constrained property.
     */
    public void setConstrained(boolean constrained) {
	this.constrained = constrained;
    }


    /**
     * Normally PropertyEditors will be found using the PropertyEditorManager.
     * However if for some reason you want to associate a particular
     * PropertyEditor with a given property, then you can do it with
     * this method.
     *
     * @param propertyEditorClass  The Class for the desired PropertyEditor.
     */
    public void setPropertyEditorClass(Class<?> propertyEditorClass) {
	propertyEditorClassRef = createReference(propertyEditorClass);
    }

    /**
     * Gets any explicit PropertyEditor Class that has been registered
     * for this property.
     *
     * @return Any explicit PropertyEditor Class that has been registered
     *		for this property.  Normally this will return "null",
     *		indicating that no special editor has been registered,
     *		so the PropertyEditorManager should be used to locate
     *		a suitable PropertyEditor.
     */
    public Class<?> getPropertyEditorClass() {
	return (Class)getObject(propertyEditorClassRef);
    }

    /**
     * Constructs an instance of a property editor using the current
     * property editor class.
     * <p>
     * If the property editor class has a public constructor that takes an
     * Object argument then it will be invoked using the bean parameter
     * as the argument. Otherwise, the default constructor will be invoked.
     *
     * @param bean the source object
     * @return a property editor instance or null if a property editor has
     *         not been defined or cannot be created
     * @since 1.5
     */
    public PropertyEditor createPropertyEditor(Object bean) {
	Object editor = null;

	Class cls = getPropertyEditorClass();
	if (cls != null) {
	    Constructor ctor = null;
	    if (bean != null) {
		try {
		    ctor = cls.getConstructor(new Class[] { Object.class });
		} catch (Exception ex) {
		    // Fall through
		}
	    }
	    try {
		if (ctor == null) {
		    editor = cls.newInstance();
		} else {
		    editor = ctor.newInstance(new Object[] { bean });
		}
	    } catch (Exception ex) {
		// A serious error has occured. 
		// Proably due to an invalid property editor.
		throw new RuntimeException("PropertyEditor not instantiated", 
					   ex);
	    }
	}
	return (PropertyEditor)editor;
    }


    /**
     * Compares this <code>PropertyDescriptor</code> against the specified object.
     * Returns true if the objects are the same. Two <code>PropertyDescriptor</code>s
     * are the same if the read, write, property types, property editor and
     * flags  are equivalent.
     *
     * @since 1.4
     */
    public boolean equals(Object obj) {
	if (this == obj) {
	    return true;
	}
	if (obj != null && obj instanceof PropertyDescriptor) {
	    PropertyDescriptor other = (PropertyDescriptor)obj;
	    Method otherReadMethod = other.getReadMethod();
	    Method otherWriteMethod = other.getWriteMethod();

	    if (!compareMethods(getReadMethod(), otherReadMethod)) {
		return false;
	    }

	    if (!compareMethods(getWriteMethod(), otherWriteMethod)) {
		return false;
	    }

	    if (getPropertyType() == other.getPropertyType() && 
		getPropertyEditorClass() == other.getPropertyEditorClass() &&
		bound == other.isBound() && constrained == other.isConstrained() &&
		writeMethodName == other.writeMethodName && 
		readMethodName == other.readMethodName) {
		return true;
	    }
	}
	return false;
    }

    /**
     * Package private helper method for Descriptor .equals methods.
     *
     * @param a first method to compare
     * @param b second method to compare
     * @return boolean to indicate that the methods are equivalent
     */
    boolean compareMethods(Method a, Method b) {
	// Note: perhaps this should be a protected method in FeatureDescriptor
	if ((a == null) != (b == null)) {
	    return false;
	}

	if (a != null && b != null) {
	    if (!a.equals(b)) {
		return false;
	    }
	}
	return true;
    }

    /**
     * Package-private constructor.
     * Merge two property descriptors.  Where they conflict, give the
     * second argument (y) priority over the first argument (x).
     *
     * @param x  The first (lower priority) PropertyDescriptor
     * @param y  The second (higher priority) PropertyDescriptor
     */
    PropertyDescriptor(PropertyDescriptor x, PropertyDescriptor y) {
	super(x,y);

	if (y.baseName != null) {
	    baseName = y.baseName;
	} else {
	    baseName = x.baseName;
	}

	if (y.readMethodName != null) {
	    readMethodName = y.readMethodName;
	} else {
	    readMethodName = x.readMethodName;
	}

	if (y.writeMethodName != null) {
	    writeMethodName = y.writeMethodName;
	} else {
	    writeMethodName = x.writeMethodName;
	}

	if (y.propertyTypeRef != null) {
	    propertyTypeRef = y.propertyTypeRef;
	} else {
	    propertyTypeRef = x.propertyTypeRef;
	}

	// Figure out the merged read method.
	Method xr = x.getReadMethod();
	Method yr = y.getReadMethod();

	// Normally give priority to y's readMethod.
	try {
	    if (yr != null && yr.getDeclaringClass() == getClass0()) {
		setReadMethod(yr);
	    } else {
		setReadMethod(xr);
	    }
	} catch (IntrospectionException ex) {
	    // fall through
	}
	    
	// However, if both x and y reference read methods in the same class,
	// give priority to a boolean "is" method over a boolean "get" method.
	if (xr != null && yr != null &&
		   xr.getDeclaringClass() == yr.getDeclaringClass() &&
		   xr.getReturnType() == boolean.class &&
		   yr.getReturnType() == boolean.class &&
		   xr.getName().indexOf("is") == 0 &&
		   yr.getName().indexOf("get") == 0) {
	    try {
		setReadMethod(xr);
	    } catch (IntrospectionException ex) {
		// fall through
	    }
	}

	Method xw = x.getWriteMethod();
	Method yw = y.getWriteMethod();

	try {
	    if (yw != null && yw.getDeclaringClass() == getClass0()) {
		setWriteMethod(yw);
	    } else {
		setWriteMethod(xw);
	    }
	} catch (IntrospectionException ex) {
	    // Fall through
	}

	if (y.getPropertyEditorClass() != null) {
	    setPropertyEditorClass(y.getPropertyEditorClass());
	} else {
	    setPropertyEditorClass(x.getPropertyEditorClass());
	}
	

	bound = x.bound | y.bound;
	constrained = x.constrained | y.constrained;
    }

    /*
     * Package-private dup constructor.
     * This must isolate the new object from any changes to the old object.
     */
    PropertyDescriptor(PropertyDescriptor old) {
	super(old);
	propertyTypeRef = old.propertyTypeRef;
	readMethodRef = old.readMethodRef;
	writeMethodRef = old.writeMethodRef;
	propertyEditorClassRef = old.propertyEditorClassRef;

	writeMethodName = old.writeMethodName;
	readMethodName = old.readMethodName;
	baseName = old.baseName;

	bound = old.bound;
	constrained = old.constrained;
    }

    /**
     * Returns the property type that corresponds to the read and write method.
     * The type precedence is given to the readMethod.
     * 
     * @return the type of the property descriptor or null if both
     *         read and write methods are null.
     * @throws IntrospectionException if the read or write method is invalid
     */
    private Class findPropertyType(Method readMethod, Method writeMethod)
	throws IntrospectionException {
	Class propertyType = null;
	try {
	    if (readMethod != null) {
		Class[] params = readMethod.getParameterTypes();
		if (params.length != 0) {
		    throw new IntrospectionException("bad read method arg count: " 
						     + readMethod);
		}
		propertyType = readMethod.getReturnType();
		if (propertyType == Void.TYPE) {
		    throw new IntrospectionException("read method " +
					readMethod.getName() + " returns void");
		}
	    }
	    if (writeMethod != null) {
		Class params[] = writeMethod.getParameterTypes();
		if (params.length != 1) {
		    throw new IntrospectionException("bad write method arg count: "
						     + writeMethod);
		}
		if (propertyType != null && propertyType != params[0]) {
		    throw new IntrospectionException("type mismatch between read and write methods");
		}
		propertyType = params[0];
	    }
	} catch (IntrospectionException ex) {
	    throw ex;
	}
	return propertyType;
    }


    /**
     * Returns a hash code value for the object. 
     * See {@link java.lang.Object#hashCode} for a complete description.
     *
     * @return a hash code value for this object.
     * @since 1.5
     */
    public int hashCode() {
	int result = 7;

	result = 37 * result + ((getPropertyType() == null) ? 0 : 
				getPropertyType().hashCode());
	result = 37 * result + ((getReadMethod() == null) ? 0 : 
				getReadMethod().hashCode());
	result = 37 * result + ((getWriteMethod() == null) ? 0 : 
				getWriteMethod().hashCode());
	result = 37 * result + ((getPropertyEditorClass() == null) ? 0 : 
				getPropertyEditorClass().hashCode());
	result = 37 * result + ((writeMethodName == null) ? 0 : 
				writeMethodName.hashCode());
	result = 37 * result + ((readMethodName == null) ? 0 : 
				readMethodName.hashCode());
	result = 37 * result + getName().hashCode();
	result = 37 * result + ((bound == false) ? 0 : 1);
	result = 37 * result + ((constrained == false) ? 0 : 1);

	return result;
    }

    // Calculate once since capitalize() is expensive.
    String getBaseName() {
	if (baseName == null) {
	    baseName = capitalize(getName());
	}
	return baseName;
    }

    /*
    public String toString() {
	String message = "name=" + getName();
	message += ", class=" + getClass0();
	message += ", type=" + getPropertyType();

	message += ", writeMethod=";
	message += writeMethodName;

	message += ", readMethod=";
	message += readMethodName;

	message += ", bound=" + bound;
	message += ", constrained=" + constrained;

	return message;
    } 
    */
}
